﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using WeifenLuo.WinFormsUI.Docking;
using System.IO;
using ICSharpCode.TextEditor.Document;
using System.Reflection;
using System.Xml;
using ICSharpCode.TextEditor;
using System.Drawing.Drawing2D;
using ICSharpCode.TextEditor.Actions;
using UnrealDebugger.Controls;

namespace UnrealDebugger.Panels
{
    public partial class Document : DockContent
    {
        #region Public Properties

        public string FileName
        {
            get 
            { 
                return this.textEditorControlDocument.FileName != null ? this.textEditorControlDocument.FileName : "Invalid"; 
            }

            set
            { 
                try 
                {
                    UnrealDebuggerIDE.Instance.RaiseDocumentClosed(this);

                    this.textEditorControlDocument.LoadFile(value, true, true);

                    this.textEditorControlDocument.IsReadOnly = false;
                    ConvertTabsToSpaces action = new ConvertTabsToSpaces();
                    action.Execute(this.textEditorControlDocument.ActiveTextAreaControl.TextArea);
                    this.textEditorControlDocument.IsReadOnly = true;

                    this.Name = Path.GetFileName(value);
                    this.Text = Path.GetFileName(value);

                    this._outlineControl = new OutlineControl();
                    this._outlineControl.ParseScript(value);

                    UnrealDebuggerIDE.Instance.RaiseDocumentOpened(this);
                } 
                catch (Exception e) 
                {
                    ExceptionMessageBox msgBox = new ExceptionMessageBox(e);
                    msgBox.ShowDialog();
                } 
            }
        }

        private OutlineControl _outlineControl = null;
        public OutlineControl OutlineControl 
        { 
            get 
            { 
                return _outlineControl; 
            } 
        }

        public string Package
        {
            get
            {
                string[] tokens = FileName.Split(new char[] { '\\', '/' }, StringSplitOptions.RemoveEmptyEntries);

                for (int idx = 1; idx < tokens.Length; ++idx)
                {
                    if (string.Compare(tokens[idx], "Classes", true) == 0)
                    {
                        return tokens[idx - 1];
                    }
                }

                return "";
            }
        }

        public string ClassName
        {
            get
            {
                return Path.GetFileNameWithoutExtension(FileName);
            }
        }

        public string FullClassName
        {
            get
            {
                return Package.Length > 0 ? Package + "." + ClassName : ClassName;
            }
        }

        public bool IsScriptClass
        {
            get
            {
                return Package.Length > 0 &&
                    string.Compare(Path.GetExtension(this.textEditorControlDocument.FileName), ".uc", true) == 0;
            }
        }

        public TextEditorControl TextEditor
        {
            get
            {
                return this.textEditorControlDocument;
            }
        }

        #endregion

        private DebuggerMargin _debuggerMargin = null;
        public DebuggerMargin DebuggerMargin { get { return _debuggerMargin; } }

        #region Document Highlighting

        public class DocumentSyntaxModeProvider : ISyntaxModeFileProvider
        {
            List<SyntaxMode> syntaxModes = null;

            public ICollection<SyntaxMode> SyntaxModes
            {
                get
                {
                    return syntaxModes;
                }
            }

            public DocumentSyntaxModeProvider()
            {
                Assembly assembly = typeof(Document).Assembly;

                Stream syntaxModeStream = assembly.GetManifestResourceStream("UnrealDebugger.Resources.SyntaxModes.xml");
                if (syntaxModeStream != null)
                {
                    syntaxModes = SyntaxMode.GetSyntaxModes(syntaxModeStream);
                }
                else
                {
                    syntaxModes = new List<SyntaxMode>();
                }
            }

            public XmlTextReader GetSyntaxModeFile(SyntaxMode syntaxMode)
            {
                Assembly assembly = typeof(Document).Assembly;
                return new XmlTextReader(assembly.GetManifestResourceStream("UnrealDebugger.Resources." + syntaxMode.FileName));
            }

            public void UpdateSyntaxModeList()
            {
                // resources don't change during runtime
            }
        }

        static Document()
        {
            DocumentSyntaxModeProvider fsmProvider = new DocumentSyntaxModeProvider();
            HighlightingManager.Manager.AddSyntaxModeFileProvider(fsmProvider);
        }

        #endregion

        public Document()
        {
            UnrealDebuggerIDE.Instance.OnProjectLoaded += new UnrealDebuggerIDE.ProjectLoadedDelegate(OnProjectLoaded);
            UnrealDebuggerIDE.Instance.OnEditorPropertiesChanged += new UnrealDebuggerIDE.EditorPropertiesChangedDelegate(OnEditorPropertiesChanged);
            UnrealDebuggerIDE.Instance.OnBreakpointsChanged += new UnrealDebuggerIDE.BreakpointsChangedDelegate(OnBreakpointsChanged);
            UnrealDebuggerIDE.Instance.OnDebuggerDetached += new UnrealDebuggerIDE.DebuggerDetachedDelegate(OnDebuggerDetached);

            InitializeComponent();

            this.textEditorControlDocument.IsReadOnly = true;
            this.textEditorControlDocument.IsIconBarVisible = true;
            this.textEditorControlDocument.LineViewerStyle = LineViewerStyle.FullRow;
            this.textEditorControlDocument.Font = UnrealDebuggerIDE.Instance.Project.EditorFont;

            TextArea textArea = this.textEditorControlDocument.ActiveTextAreaControl.TextArea;
            _debuggerMargin = new DebuggerMargin(this, textArea);
            textArea.InsertLeftMargin(1, _debuggerMargin);

            this.DockAreas = WeifenLuo.WinFormsUI.Docking.DockAreas.Document;
        }

        protected override string GetPersistString()
        {
            return GetType().ToString() + "," + FileName;
        }

        #region Document Management

        public int GetCaretLine()
        {
            return this.textEditorControlDocument.ActiveTextAreaControl.Caret.Line;
        }

        public void GotoLine(int lineNo, int highlight)
        {
            this.textEditorControlDocument.ActiveTextAreaControl.Caret.Line = Math.Max(lineNo - 1, 0);

            TextArea textArea = this.textEditorControlDocument.ActiveTextAreaControl.TextArea;
            textArea.Refresh(_debuggerMargin);
        }

        public void ToggleBreakpointOnLine(int lineNo, Keys modifierKeys)
        {
            Breakpoint selectedBreakpoint = null;

            MultiMap<string, Breakpoint> breakpoints = UnrealDebuggerIDE.Instance.Project.Breakpoints;
            foreach (Breakpoint breakpoint in breakpoints[this.FullClassName.ToUpper()])
            {
                if (breakpoint.LineNo - 1 == lineNo)
                {
                    selectedBreakpoint = breakpoint;
                    break;
                }
            }

            if (selectedBreakpoint != null)
            {
                if ((modifierKeys & Keys.Control) == Keys.Control)
                {
                    bool result = false;
                    if (selectedBreakpoint.Enabled) { result = UnrealDebuggerIDE.Commands.DisconnectBreakpoint(selectedBreakpoint.ClassName, selectedBreakpoint.LineNo); }
                    else { result = UnrealDebuggerIDE.Commands.ConnectBreakpoint(this.FullClassName, lineNo + 1); }

                    if (result)
                    {
                        UnrealDebuggerIDE.Actions.EnableBreakpoint(this.FullClassName, lineNo + 1, !selectedBreakpoint.Enabled);
                    }
                }
                else
                {
                    UnrealDebuggerIDE.Commands.DisconnectBreakpoint(selectedBreakpoint.ClassName, selectedBreakpoint.LineNo);
                }
            }
            else
            {
                UnrealDebuggerIDE.Commands.ConnectBreakpoint(this.FullClassName, lineNo + 1);
            }
        }

        public void OnProjectLoaded(string filename)
        {
            this.textEditorControlDocument.Font = UnrealDebuggerIDE.Instance.Project.EditorFont;
        }

        public void OnEditorPropertiesChanged()
        {
            this.textEditorControlDocument.Font = UnrealDebuggerIDE.Instance.Project.EditorFont;
        }

        public void OnBreakpointsChanged(MultiMap<string, Breakpoint> breakpoints)
        {
            TextArea textArea = this.textEditorControlDocument.ActiveTextAreaControl.TextArea;
            textArea.Refresh(_debuggerMargin);
        }

        void OnDebuggerDetached()
        {
            TextArea textArea = this.textEditorControlDocument.ActiveTextAreaControl.TextArea;
            textArea.Refresh(_debuggerMargin);
        }

        #endregion

        private void Document_FormClosing(object sender, FormClosingEventArgs e)
        {
            UnrealDebuggerIDE.Instance.RaiseDocumentClosed(this);

            UnrealDebuggerIDE.Instance.OnProjectLoaded -= new UnrealDebuggerIDE.ProjectLoadedDelegate(OnProjectLoaded);
            UnrealDebuggerIDE.Instance.OnEditorPropertiesChanged -= new UnrealDebuggerIDE.EditorPropertiesChangedDelegate(OnEditorPropertiesChanged);
            UnrealDebuggerIDE.Instance.OnBreakpointsChanged -= new UnrealDebuggerIDE.BreakpointsChangedDelegate(OnBreakpointsChanged);
            UnrealDebuggerIDE.Instance.OnDebuggerDetached -= new UnrealDebuggerIDE.DebuggerDetachedDelegate(OnDebuggerDetached);
        }

        private void Document_Activated(object sender, EventArgs e)
        {
            UnrealDebuggerIDE.Instance.RaiseDocumentActivated(this);
        }
    }

    public class DebuggerMargin : AbstractMargin
    {
        const int iconBarWidth = 18;
        Document doc = null;

        static readonly Size iconBarSize = new Size(iconBarWidth, -1);

        public override Size Size
        {
            get
            {
                return iconBarSize;
            }
        }

        public override bool IsVisible
        {
            get
            {
                return textArea.TextEditorProperties.IsIconBarVisible;
            }
        }


        public DebuggerMargin(Document doc, TextArea textArea)
            : base(textArea)
        {
            this.doc = doc;
        }

        public override void Paint(Graphics g, Rectangle rect)
        {
            if (rect.Width <= 0 || rect.Height <= 0)
            {
                return;
            }

            // paint background
            g.FillRectangle(SystemBrushes.Control, new Rectangle(drawingPosition.X, rect.Top, drawingPosition.Width - 1, rect.Height));
            g.DrawLine(SystemPens.ControlDark, base.drawingPosition.Right - 1, rect.Top, base.drawingPosition.Right - 1, rect.Bottom);

            // paint breakpoints
            MultiMap<string, Breakpoint> breakpoints = UnrealDebuggerIDE.Instance.Project.Breakpoints;
            foreach (Breakpoint breakpoint in breakpoints[this.doc.FullClassName.ToUpper()])
            {
                int lineNumber = textArea.Document.GetVisibleLine(breakpoint.LineNo - 1);
                int lineHeight = textArea.TextView.FontHeight;
                int yPos = (int)(lineNumber * lineHeight) - textArea.VirtualTop.Y;

                if (IsLineInsideRegion(yPos, yPos + lineHeight, rect.Y, rect.Bottom))
                {
                    if (lineNumber != textArea.Document.GetVisibleLine(breakpoint.LineNo - 2))
                    {
                        DrawBreakpoint(g, yPos, breakpoint.Enabled, breakpoint.Healthy);
                    }
                }
            }

            // paint debugger line
            if (this.doc == UnrealDebuggerIDE.Instance.ActiveDocument && UnrealDebuggerIDE.Instance.ActiveLine > 0)
            {
                int lineNumber = textArea.Document.GetVisibleLine(UnrealDebuggerIDE.Instance.ActiveLine - 1);
                int lineHeight = textArea.TextView.FontHeight;
                int yPos = (int)(lineNumber * lineHeight) - textArea.VirtualTop.Y;

                if (IsLineInsideRegion(yPos, yPos + lineHeight, rect.Y, rect.Bottom))
                {
                    if (lineNumber != textArea.Document.GetVisibleLine(UnrealDebuggerIDE.Instance.ActiveLine - 2))
                    {
                        DrawArrow(g, yPos);
                    }
                }
            }

            base.Paint(g, rect);
        }

        public override void HandleMouseDown(Point mousePos, MouseButtons mouseButtons)
        {
            int clickedVisibleLine = (mousePos.Y + textArea.VirtualTop.Y) / textArea.TextView.FontHeight;
            int lineNumber = textArea.Document.GetFirstLogicalLine(clickedVisibleLine);

            if (this.doc.IsScriptClass && (mouseButtons & MouseButtons.Left) == MouseButtons.Left)
            {
                this.doc.ToggleBreakpointOnLine(lineNumber, Control.ModifierKeys);
            }

            base.HandleMouseDown(mousePos, mouseButtons);
        }

        #region Drawing functions

        public void DrawBreakpoint(Graphics g, int y, bool isEnabled, bool isHealthy)
        {
            int diameter = Math.Min(iconBarWidth - 2, textArea.TextView.FontHeight);
            Rectangle rect = new Rectangle(base.drawingPosition.X + 1,
                                           y + (textArea.TextView.FontHeight - diameter) / 2,
                                           diameter,
                                           diameter);


            using (GraphicsPath path = new GraphicsPath())
            {
                path.AddEllipse(rect);
                using (PathGradientBrush pthGrBrush = new PathGradientBrush(path))
                {
                    pthGrBrush.CenterPoint = new PointF(rect.Left + rect.Width / 3, rect.Top + rect.Height / 3);
                    pthGrBrush.CenterColor = Color.MistyRose;
                    Color[] colors = { isHealthy ? Color.Firebrick : Color.Olive };
                    pthGrBrush.SurroundColors = colors;

                    if (isEnabled)
                    {
                        g.FillEllipse(pthGrBrush, rect);
                    }
                    else
                    {
                        g.FillEllipse(SystemBrushes.Control, rect);
                        using (Pen pen = new Pen(pthGrBrush))
                        {
                            g.DrawEllipse(pen, new Rectangle(rect.X + 1, rect.Y + 1, rect.Width - 2, rect.Height - 2));
                        }
                    }
                }
            }
        }

        public void DrawBookmark(Graphics g, int y, bool isEnabled)
        {
            int delta = textArea.TextView.FontHeight / 8;
            Rectangle rect = new Rectangle(base.drawingPosition.X + 1, y + delta, base.drawingPosition.Width - 4, textArea.TextView.FontHeight - delta * 2);

            if (isEnabled)
            {
                using (Brush brush = new LinearGradientBrush(new Point(rect.Left, rect.Top),
                                                             new Point(rect.Right, rect.Bottom),
                                                             Color.SkyBlue,
                                                             Color.White))
                {
                    FillRoundRect(g, brush, rect);
                }
            }
            else
            {
                FillRoundRect(g, Brushes.White, rect);
            }
            using (Brush brush = new LinearGradientBrush(new Point(rect.Left, rect.Top),
                                                         new Point(rect.Right, rect.Bottom),
                                                         Color.SkyBlue,
                                                         Color.Blue))
            {
                using (Pen pen = new Pen(brush))
                {
                    DrawRoundRect(g, pen, rect);
                }
            }
        }

        public void DrawArrow(Graphics g, int y)
        {
            int delta = textArea.TextView.FontHeight / 8;
            Rectangle rect = new Rectangle(base.drawingPosition.X + 1, y + delta, base.drawingPosition.Width - 4, textArea.TextView.FontHeight - delta * 2);
            using (Brush brush = new LinearGradientBrush(new Point(rect.Left, rect.Top),
                                                         new Point(rect.Right, rect.Bottom),
                                                         Color.LightYellow,
                                                         Color.Yellow))
            {
                FillArrow(g, brush, rect);
            }

            using (Brush brush = new LinearGradientBrush(new Point(rect.Left, rect.Top),
                                                         new Point(rect.Right, rect.Bottom),
                                                         Color.Yellow,
                                                         Color.Brown))
            {
                using (Pen pen = new Pen(brush))
                {
                    DrawArrow(g, pen, rect);
                }
            }
        }

        GraphicsPath CreateArrowGraphicsPath(Rectangle r)
        {
            GraphicsPath gp = new GraphicsPath();
            int halfX = r.Width / 2;
            int halfY = r.Height / 2;
            gp.AddLine(r.X, r.Y + halfY / 2, r.X + halfX, r.Y + halfY / 2);
            gp.AddLine(r.X + halfX, r.Y + halfY / 2, r.X + halfX, r.Y);
            gp.AddLine(r.X + halfX, r.Y, r.Right, r.Y + halfY);
            gp.AddLine(r.Right, r.Y + halfY, r.X + halfX, r.Bottom);
            gp.AddLine(r.X + halfX, r.Bottom, r.X + halfX, r.Bottom - halfY / 2);
            gp.AddLine(r.X + halfX, r.Bottom - halfY / 2, r.X, r.Bottom - halfY / 2);
            gp.AddLine(r.X, r.Bottom - halfY / 2, r.X, r.Y + halfY / 2);
            gp.CloseFigure();
            return gp;
        }

        GraphicsPath CreateRoundRectGraphicsPath(Rectangle r)
        {
            GraphicsPath gp = new GraphicsPath();
            int radius = r.Width / 2;
            gp.AddLine(r.X + radius, r.Y, r.Right - radius, r.Y);
            gp.AddArc(r.Right - radius, r.Y, radius, radius, 270, 90);

            gp.AddLine(r.Right, r.Y + radius, r.Right, r.Bottom - radius);
            gp.AddArc(r.Right - radius, r.Bottom - radius, radius, radius, 0, 90);

            gp.AddLine(r.Right - radius, r.Bottom, r.X + radius, r.Bottom);
            gp.AddArc(r.X, r.Bottom - radius, radius, radius, 90, 90);

            gp.AddLine(r.X, r.Bottom - radius, r.X, r.Y + radius);
            gp.AddArc(r.X, r.Y, radius, radius, 180, 90);

            gp.CloseFigure();
            return gp;
        }

        void DrawRoundRect(Graphics g, Pen p, Rectangle r)
        {
            using (GraphicsPath gp = CreateRoundRectGraphicsPath(r))
            {
                g.DrawPath(p, gp);
            }
        }

        void FillRoundRect(Graphics g, Brush b, Rectangle r)
        {
            using (GraphicsPath gp = CreateRoundRectGraphicsPath(r))
            {
                g.FillPath(b, gp);
            }
        }

        void DrawArrow(Graphics g, Pen p, Rectangle r)
        {
            using (GraphicsPath gp = CreateArrowGraphicsPath(r))
            {
                g.DrawPath(p, gp);
            }
        }

        void FillArrow(Graphics g, Brush b, Rectangle r)
        {
            using (GraphicsPath gp = CreateArrowGraphicsPath(r))
            {
                g.FillPath(b, gp);
            }
        }

        #endregion

        static bool IsLineInsideRegion(int top, int bottom, int regionTop, int regionBottom)
        {
            if (top >= regionTop && top <= regionBottom)
            {
                // Region overlaps the line's top edge.
                return true;
            }
            else if (regionTop > top && regionTop < bottom)
            {
                // Region's top edge inside line.
                return true;
            }
            return false;
        }
    }
}
